其實很多的效能優化技巧都擁有「用了不一定會讓效能變得比較快,就算有可能也是使用者難以感知的微幅進步,甚至如果使用不當會讓網頁效能變的更差」的特性。
不過對於 HTTP 這個應用層協議的優化建議卻是
盡可能將 HTTP 升級,因為效能真的會差很多。
傳說中使用 HTTP/2 的網站效能會比 HTTP/1.1 提升非常多,今天的目標就是理解 HTTP/1.1 的限制與 HTTP/2 甚至 HTTP/3 的可以帶來的改變。雖然嚴格來說要做到升級得透過後端的幫忙,但因為前端開發者其實會第一線接觸 HTTP 的協定,HTTP 的升級也可能讓一些傳統的效能優化方式變的過時且失去意義,因此我還是把這個主題拉到前端優化系列文當中分享,畢竟身為一個專業的前端開發者,這些 Networking 的概念還是很重要的!
這邊先假設讀者都對 TCP 協定建立連線時的三次握手與斷開連線時的四次揮手都有都有基本的了解了,如果不懂的讀者建議先看這篇文章。
至於 HTTP/1.0 因為已經太過古老我就不再多贅述,因此會直接從現在仍然有許多網站在使用的 HTTP/1.1 開始看起。
HTTP/1.1 只能透過串行的方式處理請求,代表每個請求必須等排在前面的請求處理完後才能發送出去。
為了提升 HTTP 1.X 的效能,曾經出現一個叫做 HTTP pipelining 的機制,允許一次發送「一組」請求,而不用像原本一樣等待前一個請求回傳後才能開始處理下一個請求,不過這個 HTTP pipelining 的機制,只能按照發送的順序來接收 response,例如說用 pipelining 的技術發出兩個請求,第二個請求很快只需要 300ms,第一個請求卻需要花費到 5 秒,因為要按照發送的順序來回傳,第二個請求就會被第一個耗時的請求給 block,就算處理完了也不能先回傳,這就是所謂的 head-of-line blocking (HOL)。除了 HOL 問題外,HTTP pipelining 機制還有一些限制例如只有 GET 請求與 HEAD 請求才能使用,又或是 proxy server 的支援度並不高...等等,導致 HTTP pipelining 機制可以說是失敗的,目前各家瀏覽器預設也是關閉 HTTP piplining 機制。
因為前面提及的串行的限制,有人可能會想說那就多開幾個連線就好,但是瀏覽器通常會設定對一個域名同時只能有一定數量的 TCP 連線,例如 Google Chrome 對一個域名同時間請求數量的限制就是 6 個,總連線數的限制是 10 個,當超過瀏覽器限制的數量時,請求就會被瀏覽器放到 queue 裡面排隊。在瀏覽器 Devtool 的 network waterfall 欄位可以看到每個請求經過 queueing 的時間。
雖然某種程度上它算是達到平行處理請求,但其他請求仍然受到了 HOL 的限制,必須等到這些請求被處理完才能離開 waiting queue 進而被處理。
在 HTTP 1.x 中,request 與 response 的 header 是沒辦法經過壓縮的。不要認為只需要壓縮 request response body 就夠了,request header 的大小也有可能來到 2, 3 KB,而且許多 request 的 header 內容其實是重複的,沒有壓縮的狀況下來回傳輸也會造成效能的耗損或延遲。
因為以上說明的種種限制,衍伸出了一個效能優化的法則:「盡量減少 HTTP 請求的數量。」
這邊指的減少請求數量並不是指盡量避免發出不必要的請求例如 debounce 或 throttle (後面篇章會介紹)的類型,而是指像在系列文前段提過的 CSS Sprites 或是 bundling 等將一些必要資源從原本應該要分段載入的方式聚集成較少量請求的優化方式。
另外還有一種優化技巧是 Domain Sharding,為了避免剛剛提及瀏覽器對於相同 domain 請求的數量限制,而故意將要請求的資源分散到不同 domain 上的技巧,這樣各位讀者是不是更了解這些效能優化技術存在的原因了呢?
其實現在 HTTP/2(簡稱 H2)的使用率已經非常非常高了,一些常用的網站都已經在使用 H2。
H2 相比於 HTTP 1.x,最大的提升就在於「效能」,夠直接暴力了吧 ?
H2 解決了在 HTTP 1.x 會遇到的兩大問題:
很多人會誤以為 H2 可以解決 TCP level head-of-line blocking (HOL) 的問題,但這邊大家得記住
H2 沒有解決 TCP HOL 問題!
H2 沒有解決 TCP HOL 問題!
H2 沒有解決 TCP HOL 問題!
而是只解決了 HTTP pipelining 的 HOL 問題。
我們知道在 HTTP 1.1 雖然可以用 gzip, brotil 等壓縮方式,但只能針對「body」做壓縮,header 並不能壓縮,這個問題終於在 H2 被解決。
HTTP piplining 是一個為了解決 HTTP/1.1 只能串行處理請求而誕生的機制,雖然它可以同時發出多個請求,卻因為必須按照請求的順序回傳,導致仍然會有阻塞的狀況而宣告失敗,而 H2 的 Multiplexing 則可以解決 HTTP piplining 阻塞的問題。
在 H2 中新增了兩個觀念:stream 與 frame。
在談這兩個觀念以前,得先講講 H2 對封包結構的調整,這也是 H2 性能可以提升的核心原因。
在 HTTP/1.x 中是使用文本格式,而 HTTP/2 則將所有傳輸的訊息分割為更小的消息和幀,並採用二進位格式進行編碼,同時也將封包結構重新定義成 frame 這個概念。這個新的機制改變了 client 與 server 之間交換數據的方式。
Stream 跟大家腦中想的應該差不多,有點類似一個串流管道的概念,frame 則是在裡面流動傳輸的單位。每一個 frame 都會有一個識別碼來分別是來自哪一個 stream。
重點就在於這個 stream 的識別碼,有了這個識別碼,就算回應順序跟請求順序不一樣,仍然可以根據 frame 的 stream 識別碼,重新組合出請求的資料,這就解決了 HTTP Piplining 請求會因為順序被阻塞的問題。
有了這個機制,H2 可以達到以下這些優點:
從多個圖片請求來看更可以看出 H2 帶來的效能提升
有興趣的讀者可以到這個網站玩玩。
以往我們熟悉的請求資源的方式應該是由 client 端發送請求,要幾個資源就發幾個,server 端收到請求後再回傳對應的資源。
但這樣其實有點沒有效率,在 H2 中多了一個 Server Push 的技術,server 可以從 client 發出的請求得知它可能還會需要哪些資源,而不等 client parse HTML 後發現要什麼資源再發出請求,直接主動將這些資源傳給 client。
不過關於 Server Push 這個技術其實並沒有看起來那麼美好,首先是需要開發者自己定義哪些檔案需要 server 主動傳送給 client 端,再來是有一派說法認為用 Server Push 推送靜態資源並不是一件很有意義的事,一方面是像 CSS 檔案通常會放在 HTML 的前半部,很快就會被 parse 到,那麼使用 Server Push 的時間並不會相差多少,再來是目前大多數 CDN Provider 沒有支援 Server Push,要使用 Server Push 除了得犧牲 CDN 的好處外,也得使用自己的伺服器的資源。最後是靜態資源通常都會被瀏覽器快取,但 Server Push 不會管這個資源是不是已經被快取,一律會進行推送,這就造成網路頻寬的浪費。
Server Push 能推送的資源是有限制的,必須要可以被快取且不能帶有 response body,因此只能推送 GET 與 HEAD 的請求。
在沒有那麼把握可以採用 Server Push 時,也可以考慮使用之前介紹過的 resource hint 例如 prefetch,讓瀏覽器來替我們處理這些問題。
前面提到過因為 HTTP 1.1 的限制,衍伸出了一些富有創意的效能優化技巧,例如 CSS Sprites 或是 inline scripting 這些減少發出 HTTP Request 的方式,但這些技巧都帶來一些 trade off,例如使用 Image Sprites 需要注意單一檔案的大小,也會讓程式變得較不好維護,inline 的寫法則會導致沒辦法利用瀏覽器的快取機制。而這些技巧在使用 H2 後,因為 H2 Multiplexing 的特性,基本上已經沒什麼存在的必要了。
要升級成 HTTP 2 其實我們不用對現有的程式碼做任何改動,只需要
如果覺得 Web Server 要設定 TLS 又要設定 H2 有點太麻煩,也可以偷吃步使用像 Cloudflare 這樣的 CDN 服務,可以使用免費的 TLS 憑證與 H2/H3 連線,不過注意只有 client 端到 CDN domain 這段是走 H2/H3 連線,CDN 到 origin server 這段目前還是走 HTTP 1.1。有興趣的讀者可以參考這裡。
看起來要升級並不困難,但對於原本並不是全站使用 HTTPS 的網站來說,可能是一個看起來深不見底的大坑,因為要使用 H2 必須替換成 HTTPS 的通訊協定,這時就要注意Mixed content 問題,一不注意可能網站就因為資源請求被瀏覽器擋住而暫時掛掉了,至於要如何有效率解決 Mixed content 問題可以參考這篇文章。
我們知道在 H2 中 TCP 的 Head-of-line blocking 這個問題還是沒辦法被解決,後來推出的 HTTP 3 (簡稱 H3) 選擇直接將傳輸層的 TCP 給替換掉來解決這個問題。
至於把 TCP 換掉後用什麼技術替代呢?答案是以 UDP 為基礎的 QUIC。稍微接觸過 Networking 觀念的讀者應該都知道 UDP 是不可靠的傳輸機制,而 QUIC 則是將不可靠的 UDP 改良為可靠性的傳輸。
關於 H3 今天就點到為止,讀者可以先記住它是改為使用以 UDP 為基礎的 QUIC 機制來解決 TCP head-of-line blocking 的問題,目前 H3 的支援度還沒有那麼普及,不過可想而知它在未來必定會越來越普遍,目前一些網站也已經在使用 H3 囉~
今天介紹的 Networking 觀念雖然不是單純靠前端開發者就可以處理的問題,但我覺得有一個重點是 HTTP 版本的提升可以「讓網頁效能有巨幅且顯著的成長」。透過今天的介紹讀者應該也可以了解原有版本的一些限制以及新版本嘗試解決的問題,新版本的協定例如 H3 雖然還有許多網站沒有採用或是設備還沒有支援,但舊有版本例如 HTTP 1.1 被汰換掉只是遲早的事情,目前的建議就是如果可以替換成 H2 就盡量升級吧!身為一個常與 HTTP 共舞的前端開發者,了解這些概念是十分重要的喔!
https://ithelp.ithome.com.tw/articles/10225751
https://developers.google.com/web/fundamentals/performance/http2
https://blog.bluetriangle.com/blocking-web-performance-villain
https://zhuanlan.zhihu.com/p/26757514
https://www.section.io/engineering-education/http3-vs-http2/
https://zhuanlan.zhihu.com/p/26757514
https://tw.alphacamp.co/blog/2016-07-12-http2
https://developers.google.com/web/fundamentals/performance/http2/